﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;

namespace ServiceAccessLayer.Infrastructure
{
    /// <summary>
    /// This class exposes RESTful CRUD functionality in a generic way, abstracting
    /// the implementation and useage details of HttpClient, HttpRequestMessage,
    /// HttpResponseMessage, ObjectContent, Formatters etc. 
    /// </summary>
    /// <typeparam name="T">This is the Type of Resource you want to work with, such as Customer, Order etc.</typeparam>
    /// <typeparam name="TResourceIdentifier">This is the type of the identifier that uniquely identifies a specific resource such as Id or Username etc.</typeparam>
    sealed class GenericRestHttpClient<T, TResourceIdentifier> : IDisposable where T : class
    {
        private bool _disposed;
        private HttpClient _httpClient;
        private readonly string _serviceBaseAddress;
        private readonly string _addressSuffix;
        private const string JsonMediaType = "application/json";

        /// <summary>
        /// The constructor requires two parameters that essentially initialize the underlying HttpClient.
        /// In a RESTful service, you might have URLs of the following nature (for a given resource - Member in this example):<para />
        /// 1. http://www.somedomain/api/members/<para />
        /// 2. http://www.somedomain/api/members/jdoe<para />
        /// Where the first URL will GET you all members, and allow you to POST new members.<para />
        /// While the second URL supports PUT and DELETE operations on a specifc member.
        /// </summary>
        /// <param name="serviceBaseAddress">As per the example, this would be "http://www.somedomain"</param>
        /// <param name="addressSuffix">As per the example, this would be "api/members/"</param>
        /// <param name="extraDataAsHeader"></param>
        public GenericRestHttpClient(string serviceBaseAddress, string addressSuffix, IEnumerable<KeyValuePair<string, IEnumerable<string>>> extraDataAsHeader = null)
        {
            _serviceBaseAddress = serviceBaseAddress;
            _addressSuffix = addressSuffix;
            _httpClient = AddExtraHeaders(serviceBaseAddress, extraDataAsHeader);
        }

        private HttpClient AddExtraHeaders(string serviceBaseAddress, IEnumerable<KeyValuePair<string, IEnumerable<string>>> extraDataAsHeader = null)
        {
            _httpClient = MakeHttpClient(serviceBaseAddress);

            //Send Extra Data with Request as Header
            if (extraDataAsHeader != null)
            {
                foreach (var keyValuePair in extraDataAsHeader)
                {
                    _httpClient.DefaultRequestHeaders.Add(keyValuePair.Key, keyValuePair.Value);
                }
            }

            return _httpClient;
        }

        private HttpClient MakeHttpClient(string serviceBaseAddress)
        {
            _httpClient =
                new HttpClient(new HttpClientHandler
                {
                    AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
                }) { BaseAddress = new Uri(serviceBaseAddress) };

            _httpClient.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(JsonMediaType));
            _httpClient.DefaultRequestHeaders.AcceptEncoding.Add(StringWithQualityHeaderValue.Parse("gzip"));
            _httpClient.DefaultRequestHeaders.AcceptEncoding.Add(StringWithQualityHeaderValue.Parse("deflate"));
            _httpClient.DefaultRequestHeaders.AcceptEncoding.Add(StringWithQualityHeaderValue.Parse("sdch"));
            _httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(new ProductHeaderValue("Matlus_HttpClient", "1.0")));

            return _httpClient;
        }

        public async Task<IEnumerable<T>> GetManyAsync()
        {
            var responseMessage = await _httpClient.GetAsync(_addressSuffix);
            responseMessage.EnsureSuccessStatusCode();
            return await responseMessage.Content.ReadAsAsync<IEnumerable<T>>();
        }

        public async Task<T> GetAsync(TResourceIdentifier identifier)
        {
            var responseMessage = await _httpClient.GetAsync(_addressSuffix + identifier);
            responseMessage.EnsureSuccessStatusCode();
            return await responseMessage.Content.ReadAsAsync<T>();
        }

        public async Task<T> GetAsync()
        {
            var responseMessage = await _httpClient.GetAsync(_addressSuffix);
            responseMessage.EnsureSuccessStatusCode();
            return await responseMessage.Content.ReadAsAsync<T>();
        }

        public async Task<T> PostAsync(T model)
        {
            var objectContent = CreateJsonObjectContent(model);
            var responseMessage = await _httpClient.PostAsync(_addressSuffix, objectContent);
            return await responseMessage.Content.ReadAsAsync<T>();
        }

        public async Task<T> PostMultipartAsync(string fileName, byte[] imageData)
        {
            using (var requestContent = new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture)))
            {
                HttpContent imageContent = new StreamContent(new MemoryStream(imageData));
                imageContent.Headers.ContentType = MediaTypeHeaderValue.Parse("image/jpeg");
                requestContent.Add(imageContent, "image", fileName);
                var responseMessage = await _httpClient.PostAsync(_addressSuffix, requestContent);
                return await responseMessage.Content.ReadAsAsync<T>();
            }
        }

        public async Task<Y> PostAsync<Y>(T model)
        {
            var objectContent = CreateJsonObjectContent(model);
            var responseMessage = await _httpClient.PostAsync(_addressSuffix, objectContent);
            return await responseMessage.Content.ReadAsAsync<Y>();
        }

        public async Task<string> GetStringAsync()
        {
            var responseMessage = await _httpClient.GetAsync(_addressSuffix);
            responseMessage.EnsureSuccessStatusCode();
            return await responseMessage.Content.ReadAsStringAsync();
        }

        public async Task<string> GetStringAsyncPost(T model)
        {
            var objectContent = CreateJsonObjectContent(model);
            var responseMessage = await _httpClient.PostAsync(_addressSuffix, objectContent);
            return await responseMessage.Content.ReadAsStringAsync();
        }

        public async Task PutAsync(TResourceIdentifier identifier, T model)
        {
            var objectContent = CreateJsonObjectContent(model);
            await _httpClient.PutAsync(_addressSuffix + identifier, objectContent);
        }

        public async Task DeleteAsync(TResourceIdentifier identifier)
        {
            await _httpClient.DeleteAsync(_addressSuffix + identifier);
        }

        private HttpContent CreateJsonObjectContent(T model)
        {
            MediaTypeFormatter mediaTypeFormatter = new JsonMediaTypeFormatter();
            MediaTypeHeaderValue mediaTypeHeaderValue = MediaTypeHeaderValue.Parse(JsonMediaType);

            HttpContent content = new ObjectContent<T>(model, mediaTypeFormatter, mediaTypeHeaderValue);
            return content;
        }

        #region IDisposable Members
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        private void Dispose(bool disposing)
        {
            if (!_disposed && disposing)
            {
                if (_httpClient != null)
                {
                    var hc = _httpClient;
                    _httpClient = null;
                    hc.Dispose();
                }
                _disposed = true;
            }
        }
        #endregion IDisposable Members
    }
}
